/*
 * Copyright 2007 The Apache Software Foundation Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

package org.apache.ibatis.ibator.internal;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;

import org.apache.ibatis.ibator.exception.ShellException;
import org.apache.ibatis.ibator.internal.util.messages.Messages;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;

/**
 * This class is used to generate a String representation of an XML document. It
 * is very much based on the class dom.Writer from the Apache Xerces examples,
 * but I've simplified and updated it for use with ibator.
 * 
 * @author Andy Clark, IBM (Original work)
 * @author Jeff Butler (derivation)
 */
public class DomWriter
{
	protected PrintWriter printWriter;

	protected boolean isXML11;

	public DomWriter()
	{
		super();
	}

	public synchronized String toString(Document document)
			throws ShellException
	{
		StringWriter sw = new StringWriter();
		printWriter = new PrintWriter(sw);
		write(document);
		String s = sw.toString();
		return s;
	}

	/** Returns a sorted list of attributes. */
	protected Attr[] sortAttributes(NamedNodeMap attrs)
	{

		int len = (attrs != null) ? attrs.getLength() : 0;
		Attr array[] = new Attr[len];
		for (int i = 0; i < len; i++)
		{
			array[i] = (Attr) attrs.item(i);
		}
		for (int i = 0; i < len - 1; i++)
		{
			String name = array[i].getNodeName();
			int index = i;
			for (int j = i + 1; j < len; j++)
			{
				String curName = array[j].getNodeName();
				if (curName.compareTo(name) < 0)
				{
					name = curName;
					index = j;
				}
			}
			if (index != i)
			{
				Attr temp = array[i];
				array[i] = array[index];
				array[index] = temp;
			}
		}

		return array;

	}

	/** Normalizes and prints the given string. */
	protected void normalizeAndPrint(String s, boolean isAttValue)
	{

		int len = (s != null) ? s.length() : 0;
		for (int i = 0; i < len; i++)
		{
			char c = s.charAt(i);
			normalizeAndPrint(c, isAttValue);
		}

	}

	/** Normalizes and print the given character. */
	protected void normalizeAndPrint(char c, boolean isAttValue)
	{

		switch (c)
		{
			case '<':
			{
				printWriter.print("&lt;"); //$NON-NLS-1$
				break;
			}
			case '>':
			{
				printWriter.print("&gt;"); //$NON-NLS-1$
				break;
			}
			case '&':
			{
				printWriter.print("&amp;"); //$NON-NLS-1$
				break;
			}
			case '"':
			{
				// A '"' that appears in character data
				// does not need to be escaped.
				if (isAttValue)
				{
					printWriter.print("&quot;"); //$NON-NLS-1$
				}
				else
				{
					printWriter.print('"');
				}
				break;
			}
			case '\r':
			{
				// If CR is part of the document's content, it
				// must not be printed as a literal otherwise
				// it would be normalized to LF when the document
				// is reparsed.
				printWriter.print("&#xD;"); //$NON-NLS-1$
				break;
			}
			default:
			{
				// In XML 1.1, control chars in the ranges [#x1-#x1F, #x7F-#x9F]
				// must be escaped.
				//
				// Escape space characters that would be normalized to #x20 in
				// attribute values
				// when the document is reparsed.
				//
				// Escape NEL (0x85) and LSEP (0x2028) that appear in content
				// if the document is XML 1.1, since they would be normalized to
				// LF
				// when the document is reparsed.
				if (isXML11
						&& ((c >= 0x01 && c <= 0x1F && c != 0x09 && c != 0x0A)
								|| (c >= 0x7F && c <= 0x9F) || c == 0x2028)
						|| isAttValue && (c == 0x09 || c == 0x0A))
				{
					printWriter.print("&#x"); //$NON-NLS-1$
					printWriter.print(Integer.toHexString(c).toUpperCase());
					printWriter.print(';');
				}
				else
				{
					printWriter.print(c);
				}
			}
		}
	}

	/** Extracts the XML version from the Document. */
	protected String getVersion(Document document)
	{
		if (document == null)
		{
			return null;
		}
		String version = null;
		Method getXMLVersion = null;
		try
		{
			getXMLVersion = document.getClass().getMethod("getXmlVersion", //$NON-NLS-1$
					new Class[]
					{});
			// If Document class implements DOM L3, this method will exist.
			if (getXMLVersion != null)
			{
				version = (String) getXMLVersion.invoke(document,
						(Object[]) null);
			}
		}
		catch (Exception e)
		{
			// Either this locator object doesn't have
			// this method, or we're on an old JDK.
		}
		return version;
	}

	protected void writeAnyNode(Node node) throws ShellException
	{
		// is there anything to do?
		if (node == null)
		{
			return;
		}

		short type = node.getNodeType();
		switch (type)
		{
			case Node.DOCUMENT_NODE:
				write((Document) node);
				break;

			case Node.DOCUMENT_TYPE_NODE:
				write((DocumentType) node);
				break;

			case Node.ELEMENT_NODE:
				write((Element) node);
				break;

			case Node.ENTITY_REFERENCE_NODE:
				write((EntityReference) node);
				break;

			case Node.CDATA_SECTION_NODE:
				write((CDATASection) node);
				break;

			case Node.TEXT_NODE:
				write((Text) node);
				break;

			case Node.PROCESSING_INSTRUCTION_NODE:
				write((ProcessingInstruction) node);
				break;

			case Node.COMMENT_NODE:
				write((Comment) node);
				break;

			default:
				throw new ShellException(Messages.getString(
						"RuntimeError.18", Short.toString(type))); //$NON-NLS-1$
		}
	}

	protected void write(Document node) throws ShellException
	{
		isXML11 = "1.1".equals(getVersion(node)); //$NON-NLS-1$
		if (isXML11)
		{
			printWriter.println("<?xml version=\"1.1\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$
		}
		else
		{
			printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$
		}
		printWriter.flush();
		write(node.getDoctype());
		write(node.getDocumentElement());
	}

	protected void write(DocumentType node) throws ShellException
	{
		printWriter.print("<!DOCTYPE "); //$NON-NLS-1$
		printWriter.print(node.getName());
		String publicId = node.getPublicId();
		String systemId = node.getSystemId();
		if (publicId != null)
		{
			printWriter.print(" PUBLIC \""); //$NON-NLS-1$
			printWriter.print(publicId);
			printWriter.print("\" \""); //$NON-NLS-1$
			printWriter.print(systemId);
			printWriter.print('\"');
		}
		else
			if (systemId != null)
			{
				printWriter.print(" SYSTEM \""); //$NON-NLS-1$
				printWriter.print(systemId);
				printWriter.print('"');
			}

		String internalSubset = node.getInternalSubset();
		if (internalSubset != null)
		{
			printWriter.println(" ["); //$NON-NLS-1$
			printWriter.print(internalSubset);
			printWriter.print(']');
		}
		printWriter.println('>');
	}

	protected void write(Element node) throws ShellException
	{
		printWriter.print('<');
		printWriter.print(node.getNodeName());
		Attr attrs[] = sortAttributes(node.getAttributes());
		for (Attr attr : attrs)
		{
			printWriter.print(' ');
			printWriter.print(attr.getNodeName());
			printWriter.print("=\""); //$NON-NLS-1$
			normalizeAndPrint(attr.getNodeValue(), true);
			printWriter.print('"');
		}

		if (node.getChildNodes().getLength() == 0)
		{
			printWriter.print(" />"); //$NON-NLS-1$
			printWriter.flush();
		}
		else
		{
			printWriter.print('>');
			printWriter.flush();

			Node child = node.getFirstChild();
			while (child != null)
			{
				writeAnyNode(child);
				child = child.getNextSibling();
			}

			printWriter.print("</"); //$NON-NLS-1$
			printWriter.print(node.getNodeName());
			printWriter.print('>');
			printWriter.flush();
		}
	}

	protected void write(EntityReference node)
	{
		printWriter.print('&');
		printWriter.print(node.getNodeName());
		printWriter.print(';');
		printWriter.flush();
	}

	protected void write(CDATASection node)
	{
		printWriter.print("<![CDATA["); //$NON-NLS-1$
		printWriter.print(node.getNodeValue());
		printWriter.print("]]>"); //$NON-NLS-1$
		printWriter.flush();
	}

	protected void write(Text node)
	{
		normalizeAndPrint(node.getNodeValue(), false);
		printWriter.flush();
	}

	protected void write(ProcessingInstruction node)
	{
		printWriter.print("<?"); //$NON-NLS-1$
		printWriter.print(node.getNodeName());
		String data = node.getNodeValue();
		if (data != null && data.length() > 0)
		{
			printWriter.print(' ');
			printWriter.print(data);
		}
		printWriter.print("?>"); //$NON-NLS-1$
		printWriter.flush();
	}

	protected void write(Comment node)
	{
		printWriter.print("<!--"); //$NON-NLS-1$
		String comment = node.getNodeValue();
		if (comment != null && comment.length() > 0)
		{
			printWriter.print(comment);
		}
		printWriter.print("-->"); //$NON-NLS-1$
		printWriter.flush();
	}
}
